<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>对象扩展</title>
</head>
<body>
<script type="text/javascript">
    {
        /**属性的简洁表示法*/
        let foo = 'bar';
        let baz = {foo};
        console.log(baz); // {foo: "bar"}
        console.log({foo: foo}); // {foo: "bar"}

        function es5(x, y) {
            return {x: x, y: y};
        }

        function es6(x, y) {
            return {x, y}
        }

        console.log(es5(1, 2), es6(1, 2)); // {x: 1, y: 2} {x: 1, y: 2}

        /**方法简写*/
        let es5Method = {
            method: function () {
                return 'hello es5';
            }
        };
        let es6Method = {
            method() {
                return 'hello es6';
            }
        };
        console.log(es5Method.method(), es6Method.method());// hello es5, hello es6

        let birth = '2019-07-01';
        let Person = {
            name: 'Mike',
            birth, // 等同于birth: birth
            hello() {
                console.log('my name is ' + this.name + ', my birth is ' + `${birth}`);
            }
        };
        Person.hello(); //my name is Mike, my birth is 2019-07-01

        function getPoint() {
            let x = 1, y = 10;
            return {x, y}
        }

        console.log(getPoint()); // {x: 1, y: 10}
    }
    {
        /**属性名表达式*/
        // 方法一：obj.foo=true
        // 方法二：obj['a' + 'bc'] = 123;
        /*使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性。*/
        var obj = {foo: true, abc: 123};

        /*ES6 允许字面量定义对象时，用方法二(表达式)作为对象的属性名，即把表达式放在方括号内。*/
        let propKey = 'foo';
        let es6Obj = {
            [propKey]: true,
            ['a' + 'bc']: 123
        };
        console.log(es6Obj); // {foo: true, abc: 123}

        let lastWorld = 'last world';
        let a = {
            'first world': 'hello',
            [lastWorld]: 'world'
        };
        console.log(a['first world']); // hello
        console.log(a[lastWorld]); // world
        console.log(a['last world']); // world

        /*表达式还可以用于定义方法名*/
        let obj2 = {
            ['h' + 'ello']() {
                return 'hey';
            }
        };
        console.log(obj2.hello()); // hey
    }
    {
        /**方法的 name 属性*/
        /*函数的name属性，返回函数名。对象方法也是函数，因此也有name属性。*/
        let person = {
            sayName() {
                console.log(this.name);
            },
            get firstName() {
                return "Nicholas";
            }
        };
        console.log(person.sayName.name); // sayName
        console.log(person.firstName.name); // get firstName
    }
    {
        /** Object.is()：比较两个值是否严格相等，与严格比较运算符(===)的行为基本一致。*/
        console.log(Object.is('foo', 'foo')); // true
        console.log(Object.is({}, {})); // false
        console.log(+0 === -0, Object.is(+0, -0)); // true false
        console.log(NaN === NaN, Object.is(NaN, NaN)); // false true
    }
    {
        /** Object.assign()：用于对象的合并，将源对象(source)的所有可枚举属性，复制到目标对象(target)。
         * 第一个参数是目标对象，后面的参数都是源对象。
         */
        const target = {a: 1};
        const source1 = {b: 2};
        const source2 = {c: 3};
        console.log(Object.assign(target, source1, source2)); // {a: 1, b: 2, c: 3}
        console.log(target); // {a: 1, b: 2, c: 3}

        /*如果目标对象与源对象有同名属性，或多个源对象有同名属性，则后面的属性会覆盖前面的属性*/
        const tar = {a: 1, b: 1};
        const src1 = {b: 2, c: 2};
        const src3 = {c: 3};
        console.log(Object.assign(tar, src1, src3)); // {a: 1, b: 2, c: 3}

        /*如果只有一个参数，Object.assign会直接返回该参数*/
        const obj = {a: 1};
        console.log(Object.assign(obj)); // {a: 1}
        console.log(Object.assign(obj) === obj); // true

        /*如果该参数不是对象，则会先转成对象，然后返回*/
        console.log(typeof Object.assign(2)); // object

        /*由于undefined和null无法转成对象，所以如果它们作为参数，就会报错*/
        // console.log(Object.assign(undefined), Object.assign(null)); // Uncaught TypeError: Cannot convert undefined or null to object

        /*其他类型的值(即数值、字符串和布尔值)不在首参数，也不会报错。但是，除了字符串会以数组形式，拷贝入目标对象，其他值都不会产生效果*/
        let v1 = 'abc', v2 = true, v3 = 10;
        console.log(Object.assign({}, v1, v2, v3)); // {0: "a", 1: "b", 2: "c"}

        /*Object.assign方法实行的是浅拷贝，而不是深拷贝。也就是说，如果源对象某个属性的值是对象，那么目标对象拷贝得到的是这个对象的引用*/
        let obj1 = {a: {b: 1}};
        let obj2 = Object.assign({}, obj1);
        console.log(obj1.a.b); // 1
        console.log(obj2.a.b); // 1

        /*对于这种嵌套的对象，一旦遇到同名属性，Object.assign的处理方法是替换，而不是添加*/
        let tar2 = {a: {b: 'c', d: 'e'}};
        let sour = {a: {b: 'hello'}};
        console.log(Object.assign(tar2, sour)); // { a: { b: 'hello' } }

        /*Object.assign可以用来处理数组，但是会把数组视为对象*/
        console.log(Object.assign([1, 2, 3], [4, 5])); // [4, 5, 3]
    }
    {
        /**常见用途*/

        /*1、为对象添加属性*/
        class Point {
            constructor(x, y) {
                Object.assign(this, {x, y}); // 将x属性和y属性添加到Point类的对象实例
            }
        }

        console.log(new Point(1, 2)); // Point {x: 1, y: 2}

        /*2、为对象添加方法*/
        /*Object.assign(SomeClass.prototype, {
            someMethod(arg1, arg2) {
            ···
            },
            anotherMethod() {
            ···
            }
            // 上面代码使用了对象属性的简洁表示法，直接将两个函数放在大括号中，再使用assign方法添加到SomeClass.prototype之中。
        });*/

        /*3、克隆对象*/
        function clone(origin) {
            return Object.assign({}, origin);
        }

        console.log(clone({x: 1, y: 2})); // {x: 1, y: 2}
        /*深拷贝*/
        function deepClone(origin) {
            let originProto = Object.getPrototypeOf(origin);
            return Object.assign(Object.create(originProto), origin);
        }

        console.log(deepClone({a: {b: 1}})); // {a: {b: 1}}

        /*4、合并多个对象:将多个对象合并到某个对象*/
        const merge = (target, ...sources) => Object.assign(target, ...sources);
        console.log(merge);
        /*如果希望合并后返回一个新对象，可以改写上面函数，对一个空对象合并*/
        const merge2 = (...source) => Object.assign({}, ...source);
        console.log(merge2);

        /*5、为属性指定默认值*/
        const DEFAULTS = {
            logLevel: 0,
            outputFormat: 'html'
        };

        function processContent(options) {
            options = Object.assign({}, DEFAULTS, options);
        }

        console.log(processContent('content'));
    }
    {
        /**属性的可枚举性*/
        let obj = {foo: 123};
        console.log(Object.getOwnPropertyDescriptor(obj, 'foo')); // {value: 123, writable: true, enumerable: true, configurable: true}
        /*toString和length属性的enumerable都是false，因此for...in不会遍历到这两个继承自原型的属性*/
        console.log(Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable); // false
        console.log(Object.getOwnPropertyDescriptor([], 'length').enumerable); // false
        /*所有Class的原型的方法都是不可枚举的*/
        console.log(Object.getOwnPropertyDescriptor(class {
            foo() {
            }
        }.prototype, 'foo').enumerable); // false
    }
    {
        /**属性的遍历*/
        /*
         * 1、for...in:循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)
         * 2、Object.keys(obj):返回一个数组，包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)
         * 3、Object.getOwnPropertyNames(obj):返回一个数组，包含对象自身的所有属性(不含Symbol属性，但是包括不可枚举属性)
         * 4、Object.getOwnPropertySymbols(obj):返回一个数组，包含对象自身的所有Symbol属性
         * 5、Reflect.ownKeys(obj):返回一个数组，包含对象自身的所有属性，不管是属性名是Symbol或字符串，也不管是否可枚举
         * 以上的5种方法遍历对象的属性，都遵守同样的属性遍历的次序规则
         * 1、遍历所有属性名为数值的属性，按照数字排序。
         * 2、遍历所有属性名为字符串的属性，按照生成时间排序。
         * 3、遍历所有属性名为Symbol值的属性，按照生成时间排序。
         */
        // Reflect.ownKeys方法返回一个数组，包含了参数对象的所有属性。这个数组的属性次序是这样的，首先是数值属性2和10，其次是字符串属性b和a，最后是Symbol属性。
        console.log(Reflect.ownKeys({[Symbol()]: 0, b: 0, 10: 0, 2: 0, a: 0})); // ["2", "10", "b", "a", Symbol()]
    }
    {
        /**__proto__属性，Object.setPrototypeOf()，Object.getPrototypeOf()*/
        /*1、__proto__属性:(前后各两个下划线)用来读取或设置当前对象的prototype对象*/
        /*var obj = {
            method: function() { ... }
        };
        obj.__proto__ = someOtherObj;*/

        /*
         * 2、Object.setPrototypeOf():用来设置一个对象的prototype对象
         * 格式：Object.setPrototypeOf(object, prototype)
         * 用法：var o = Object.setPrototypeOf({}, null);
         */
        let proto = {};
        let obj = {x: 10};
        Object.setPrototypeOf(obj, proto);
        proto.y = 20;
        proto.z = 40;
        console.log(obj.x, obj.y, obj.z); // 10 20 40

        /*
        * 3、Object.getPrototypeOf():用于读取一个对象的prototype对象
        * 格式：Object.getPrototypeOf(obj)
        */
        function Rectangle() {
        }

        let rec = new Rectangle();
        console.log(Object.getPrototypeOf(rec) === Rectangle.prototype); // true
        Object.setPrototypeOf(rec, Object.prototype);
        console.log(Object.getPrototypeOf(rec) === Rectangle.prototype); // false
    }
    {
        /** Object.values()，Object.entries()，Object.keys() */
        /*1、Object.keys():返回一个数组，成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名*/
        const object = {foo: "bar", baz: 42};
        console.log(Object.keys(object)); // ["foo", "baz"]

        let {keys, values, entries} = Object;
        let obj = {a: 1, b: 2, c: 3};
        for (let key of keys(obj)) {
            console.log(key); // 'a', 'b', 'c'
        }
        for (let value of values(obj)) {
            console.log(value); // 1, 2, 3
        }
        for (let [key, value] of entries(obj)) {
            console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
        }

        /*2、Object.values():返回一个数组，成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值*/
        console.log(Object.values(object)); // ["bar", 42]
        let valuesObj = {100: 'a', 2: 'c', 7: 'b'};
        console.log(Object.values(valuesObj)); // ["c", "b", "a"]，按照数字排序
        // 参数是一个字符串，会返回各个字符组成的一个数组
        console.log(Object.values('foo')); // ["f", "o", "o"]

        /*3、Object.entries():返回一个数组，成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组*/
        console.log(Object.entries(object)); // [ ["foo", "bar"], ["baz", 42] ]
        console.log(Object.entries({[Symbol()]: 123, foo: 'abc'})); // [ [ 'foo', 'abc' ] ]
        let entriesObj = {one: 1, two: 2};
        for (let [k, v] of Object.entries(entriesObj)) {
            console.log(`${JSON.stringify(k)}: ${JSON.stringify(v)}`); // "one": 1 "two": 2
        }
        /*Object.entries方法的一个用处是，将对象转为真正的Map结构*/
        console.log(new Map(Object.entries(object))); // Map { foo: "bar", baz: 42 }
    }
    {
        /**对象的扩展运算符*/
        /*1、解构赋值*/
        let {x, y, ...z} = {x: 1, y: 2, a: 3, b: 4};
        console.log(x, y, z); // 1 2 {a: 3, b: 4}

        /*解构赋值的拷贝是浅拷贝，即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用，而不是这个值的副本*/
        let obj = {a: {b: 1}};
        let {...c} = obj;
        obj.a.b = 2;
        console.log(c.a.b); // 2

        /*解构赋值不会拷贝继承自原型对象的属性*/
        let o1 = {a: 1};
        let o2 = {b: 2};
        o2.__proto__ = o1;
        let o3 = {...o2};
        console.log(o3); // { b: 2 } 对象o3是o2的拷贝，但是只复制了o2自身的属性，没有复制它的原型对象o1的属性

        /*let o = Object.create({ e: 1, f: 2 });
        o.g = 3;
        let { e, ...{ f, g } } = o;
        console.log("e:"+e,",f:"+f+",g:"+g);*/

        /*2、扩展运算符：扩展运算符(...)用于取出参数对象的所有可遍历属性，拷贝到当前对象之中，这等同于使用Object.assign方法*/
        let d = {a: 3, b: 4};
        let n = {...d};
        console.log(n); // {a: 3, b: 4}

        let dClone = {...d};
        // 等同于
        let d2Clone = Object.assign({}, d);
        console.log(dClone, d2Clone); // {a: 3, b: 4}

        /*扩展运算符可以用于合并两个对象*/
        let ab = {...o1, ...o2};
        let ab2 = Object.assign({}, o1, o2);
        console.log(ab, ab2); // {a: 1, b: 2}

        let dWithOverrides = {...d, x: 1, y: 2};
        //等同于
        let dWithOverrides2 = {...d, ...{x: 1, y: 2}};
        let dWithOverrides3 = Object.assign({}, d, {x: 1, y: 2});
        console.log(dWithOverrides, dWithOverrides2, dWithOverrides3); // {a: 3, b: 4, x: 1, y: 2}
    }
    {
        /**
         * ES5有一个Object.getOwnPropertyDescriptor方法，返回某个对象属性的描述对象(descriptor)
         * ES7有一个提案，提出了Object.getOwnPropertyDescriptors方法，返回指定对象所有自身属性(非继承属性)的描述对象，主要是为了解决Object.assign()无法正确拷贝get属性和set属性的问题。
         */
        var p = {p: 'a'};
        console.log(Object.getOwnPropertyDescriptor(p, 'p')); // p: {value: "a", writable: true, enumerable: true, configurable: true}

        const o = {
            foo: 123,
            get bar() {
                return 'abc';
            }
        };
        console.log(Object.getOwnPropertyDescriptors(o)); // foo: {value: 123, writable: true, enumerable: true, configurable: true},bar: {get: ƒ, set: undefined, enumerable: true, configurable: true}

        /*Object.assign方法总是拷贝一个属性的值，而不会拷贝它背后的赋值方法或取值方法。*/
        const source = {
            set foo(value) {
                console.log(value);
            }
        };
        const target = {};
        console.log(Object.assign(target, source));
        console.log(Object.getOwnPropertyDescriptor(target, 'foo')); // {value: undefined, writable: true, enumerable: true, configurable: true}

        /*Object.getOwnPropertyDescriptors方法配合Object.defineProperties方法，就可以实现正确拷贝*/
        const target2 = {};
        Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
        console.log(Object.getOwnPropertyDescriptor(target2, 'foo')); // {get: undefined, set: ƒ, enumerable: true, configurable: true}
    }
</script>
</body>
</html>